import { importEntry } from 'import-html-entry';
import { concat, mergeWith, forEach } from 'lodash';
import { LifeCycles, ParcelConfigObject } from 'single-spa';
import getAddOns from './addons';
import { getMicroAppStateActions } from './globalState';
import { FrameworkConfiguration, FrameworkLifeCycles, HTMLContentRender, LifeCycleFn, LoadableApp } from './interfaces';
import { createSandbox, css } from './sandbox';
import { Deferred, getDefaultTplWrapper, getWrapperId, performanceMark, performanceMeasure, toArray, validateExportLifecycle, isEnableScopedCSS } from './utils';

function assertElementExist(element, msg) {
  if (!element) {
    if (msg) {
      throw new Error(msg);
    }

    throw new Error('[qiankun] element not existed!');
  }
}

function execHooksChain(hooks, app, global = window) {
  if (hooks.length) {
    return hooks.reduce((chain, hook) => chain.then(() => hook(app, global)), Promise.resolve());
  }

  return Promise.resolve();
}

async function validateSingularMode(validate, app) {
  return typeof validate === 'function' ? validate(app) : !!validate;
}

const supportShadowDOM = document.head.attachShadow || document.head.createShadowRoot;

function createElement(appContent, strictStyleIsolation) {
  const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  const appElement = containerElement.firstChild;

  if (strictStyleIsolation) {
    if (!supportShadowDOM) {
      console.warn('[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored!');
    } else {
      const {
        innerHTML
      } = appElement;
      appElement.innerHTML = '';
      let shadow;

      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({
          mode: 'open'
        });
      } else {
        shadow = appElement.createShadowRoot();
      }

      shadow.innerHTML = innerHTML;
    }
  }

  return appElement;
}

function getAppWrapperGetter(appName, appInstanceId, useLegacyRender, strictStyleIsolation, enableScopedCSS, elementGetter) {
  return () => {
    if (useLegacyRender) {
      if (strictStyleIsolation) throw new Error('[qiankun]: strictStyleIsolation can not be used with legacy render!');
      if (enableScopedCSS) throw new Error('[qiankun]: experimentalStyleIsolation can not be used with legacy render!');
      const appWrapper = document.getElementById(getWrapperId(appInstanceId));
      assertElementExist(appWrapper, `[qiankun] Wrapper element for ${appName} with instance ${appInstanceId} is not existed!`);
      return appWrapper;
    }

    const element = elementGetter();
    assertElementExist(element, `[qiankun] Wrapper element for ${appName} with instance ${appInstanceId} is not existed!`);

    if (enableScopedCSS) {
      const attr = element.getAttribute(css.QiankunCSSRewriteAttr);

      if (!attr) {
        element.setAttribute(css.QiankunCSSRewriteAttr, appName);
      }
    }

    if (strictStyleIsolation) {
      return element.shadowRoot;
    }

    return element;
  };
}

const rawAppendChild = HTMLElement.prototype.appendChild;
const rawRemoveChild = HTMLElement.prototype.removeChild;

function getRender(appName, appContent, container, legacyRender) {
  const render = ({
    element,
    loading
  }, phase) => {
    if (legacyRender) {
      if (process.env.NODE_ENV === 'development') {
        console.warn('[qiankun] Custom rendering function is deprecated, you can use the container element setting instead!');
      }

      return legacyRender({
        loading,
        appContent: element ? appContent : ''
      });
    }

    const containerElement = typeof container === 'string' ? document.querySelector(container) : container;

    if (phase !== 'unmounted') {
      const errorMsg = (() => {
        switch (phase) {
          case 'loading':
          case 'mounting':
            return `[qiankun] Target container with ${container} not existed while ${appName} ${phase}!`;

          case 'mounted':
            return `[qiankun] Target container with ${container} not existed after ${appName} ${phase}!`;

          default:
            return `[qiankun] Target container with ${container} not existed while ${appName} rendering!`;
        }
      })();

      assertElementExist(containerElement, errorMsg);
    }

    if (containerElement && !containerElement.contains(element)) {
      while (containerElement.firstChild) {
        rawRemoveChild.call(containerElement, containerElement.firstChild);
      }

      if (element) {
        rawAppendChild.call(containerElement, element);
      }
    }

    return undefined;
  };

  return render;
}

function getLifecyclesFromExports(scriptExports, appName, global) {
  if (validateExportLifecycle(scriptExports)) {
    return scriptExports;
  }

  if (process.env.NODE_ENV === 'development') {
    console.warn(`[qiankun] lifecycle not found from ${appName} entry exports, fallback to get from window['${appName}']`);
  }

  const globalVariableExports = global[appName];

  if (validateExportLifecycle(globalVariableExports)) {
    return globalVariableExports;
  }

  throw new Error(`[qiankun] You need to export lifecycle functions in ${appName} entry`);
}

let prevAppUnmountedDeferred;

export async function loadApp(app, configuration = {}, lifeCycles) {
  const {
    entry,
    name: appName
  } = app;
  const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`;
  const markName = `[qiankun] App ${appInstanceId} Loading`;

  if (process.env.NODE_ENV === 'development') {
    performanceMark(markName);
  }

  const {
    singular = false,
    sandbox = true,
    excludeAssetFilter,
    ...importEntryOpts
  } = configuration;
  const {
    template,
    execScripts,
    assetPublicPath
  } = await importEntry(entry, importEntryOpts);

  if (await validateSingularMode(singular, app)) {
    await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
  }

  const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
  const enableScopedCSS = isEnableScopedCSS(configuration);
  const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);
  let element = createElement(appContent, strictStyleIsolation);

  if (element && isEnableScopedCSS(configuration)) {
    const styleNodes = element.querySelectorAll('style') || [];
    forEach(styleNodes, stylesheetElement => {
      css.process(element, stylesheetElement, appName);
    });
  }

  const container = 'container' in app ? app.container : undefined;
  const legacyRender = 'render' in app ? app.render : undefined;
  const render = getRender(appName, appContent, container, legacyRender);
  render({
    element,
    loading: true
  }, 'loading');
  const containerGetter = getAppWrapperGetter(appName, appInstanceId, !!legacyRender, strictStyleIsolation, enableScopedCSS, () => element);
  let global = window;

  let mountSandbox = () => Promise.resolve();

  let unmountSandbox = () => Promise.resolve();

  if (sandbox) {
    const sandboxInstance = createSandbox(appName, containerGetter, Boolean(singular), enableScopedCSS, excludeAssetFilter);
    global = sandboxInstance.proxy;
    mountSandbox = sandboxInstance.mount;
    unmountSandbox = sandboxInstance.unmount;
  }

  const {
    beforeUnmount = [],
    afterUnmount = [],
    afterMount = [],
    beforeMount = [],
    beforeLoad = []
  } = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));
  await execHooksChain(toArray(beforeLoad), app, global);
  const scriptExports = await execScripts(global, !singular);
  const {
    bootstrap,
    mount,
    unmount,
    update
  } = getLifecyclesFromExports(scriptExports, appName, global);
  const {
    onGlobalStateChange,
    setGlobalState,
    offGlobalStateChange
  } = getMicroAppStateActions(appInstanceId);
  const parcelConfig = {
    name: appInstanceId,
    bootstrap,
    mount: [async () => {
      if (process.env.NODE_ENV === 'development') {
        const marks = performance.getEntriesByName(markName, 'mark');

        if (!marks.length) {
          performanceMark(markName);
        }
      }
    }, async () => {
      if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
        return prevAppUnmountedDeferred.promise;
      }

      return undefined;
    }, async () => {
      element = element || createElement(appContent, strictStyleIsolation);
      render({
        element,
        loading: true
      }, 'mounting');
    }, mountSandbox, async () => execHooksChain(toArray(beforeMount), app, global), async props => mount({ ...props,
      container: containerGetter(),
      setGlobalState,
      onGlobalStateChange
    }), async () => render({
      element,
      loading: false
    }, 'mounted'), async () => execHooksChain(toArray(afterMount), app, global), async () => {
      if (await validateSingularMode(singular, app)) {
        prevAppUnmountedDeferred = new Deferred();
      }
    }, async () => {
      if (process.env.NODE_ENV === 'development') {
        const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
        performanceMeasure(measureName, markName);
      }
    }],
    unmount: [async () => execHooksChain(toArray(beforeUnmount), app, global), async props => unmount({ ...props,
      container: containerGetter()
    }), unmountSandbox, async () => execHooksChain(toArray(afterUnmount), app, global), async () => {
      render({
        element: null,
        loading: false
      }, 'unmounted');
      offGlobalStateChange(appInstanceId);
      element = null;
    }, async () => {
      if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
        prevAppUnmountedDeferred.resolve();
      }
    }]
  };

  if (typeof update === 'function') {
    parcelConfig.update = update;
  }

  return parcelConfig;
}